/**
 * Copyright (C) 2012 Nuevo Banco del Chaco S.A.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ar.com.greensoft.commons.web.jsf.beans;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.PostConstruct;
import javax.faces.application.FacesMessage;
import javax.faces.application.FacesMessage.Severity;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;

import org.ajax4jsf.model.DataVisitor;
import org.ajax4jsf.model.ExtendedDataModel;
import org.ajax4jsf.model.Range;
import org.ajax4jsf.model.SequenceRange;
import org.apache.commons.lang3.StringUtils;
import org.richfaces.component.SortOrder;

import ar.com.greensoft.commons.dao.DaoException;
import ar.com.greensoft.commons.dao.GenericDao;
import ar.com.greensoft.commons.model.GsEntity;
import ar.com.greensoft.commons.search.SearchFilter;
import ar.com.greensoft.commons.web.export.ColumnValueMapper;
import ar.com.greensoft.commons.web.export.ExportColInfo;
import ar.com.greensoft.commons.web.export.ResultExporter;
import ar.com.greensoft.commons.web.export.xls.ExcelResultExporter;
import ar.com.greensoft.commons.web.jsf.CrudFacade;
import ar.com.greensoft.commons.web.jsf.converters.GenericConverter;
import ar.com.greensoft.commons.web.jsf.utils.FacesUtil;
import ar.com.greensoft.commons.web.jsf.utils.JSFMenuUtil;
import ar.com.greensoft.commons.web.jsf.utils.NavigationConstants;
import ar.com.greensoft.commons.web.jsf.utils.ResourceBundleUtil;

/**
 * @author Jorge E. Villaverde
 * @version 0.0.1
 * @since 0.0.1
 */
public abstract class AbstractCrudFacadeBean <T extends GsEntity, PK extends Serializable>  
	implements CrudFacade<T>, NavigationConstants, Serializable{

	protected static final String REPEATED_ENTITY_ERROR_MESSAGE_SUFIX = "RepeatedEntityErrorMessage";
	protected static final String REMOVED_MESSAGE_SUFIX = "RemovedMessage";
	protected static final String UPDATED_MESSAGE_SUFIX = "UpdatedMessage";
	protected static final String ADDED_MESSAGE_SUFIX = "AddedMessage";
	protected static final String DEFAULT_VALIDATION_MESSAGE = "ar.com.greensoft.validation.error";

	/**
	 * 
	 */
	private static final long serialVersionUID = 288311791069787977L;
	/**
	 * Logger para las operaciones de la fachada.
	 */
	protected Logger logger = Logger.getLogger(AbstractCrudFacadeBean.class.getName());

	private static final String SORT_PROPERTY_PARAMETER = "sortProperty";
	
	/**
	 * Facaded Entity 
	 */
	private T entity;

	/**
	 * Data Model
	 */
	private EntityDataModel dataModel;

	/**
	 * <code>true</code> if editing an existing entity. <code>false</code> otherwise.
	 */
	private boolean editing = false;
	    
    /**
     * Cancel View
     */
    protected String cancelView;
    
    /**
     * Return View
     */
    protected String persistReturnView;

    /**
	 * Search Filter
	 */
	protected SearchFilter searchFilter;
	
	/**
	 * Selected variable
	 */
	private boolean selected = false;
	/**
	 * Sort Orders
	 */
	private Map<String, SortOrder> sortsOrders;
	/**
	 * Sort Priorities
	 */
	private List<String> sortPriorities;
	/**
	 * Enable Multible Sorting
	 */
	private boolean multipleSorting = false;
	/**
	 * Entity Primary Key 
	 */
	private PK id;
	/**
	 * Mensaje de Validación a mostrar al usuario
	 */
	private String validationErrorMessage;
	
	
	@PostConstruct
	public void createBean() {
		this.editing = false;
		this.id = null;
		this.sortsOrders = new HashMap<String, SortOrder>();
		this.sortPriorities = new ArrayList<String>();
		setEntity(newEntity());
	}
  	
	@Override
	public T getEntity() {
		return entity;
	}
	
	public void setEntity(T entity) {
		this.entity = entity;
	}
	
	public PK getId() {
		return id;
	}

	public void setId(PK id) {
		this.entity = null;
		this.id = id;
	}	
	
	@Override
	public List<T> getAllEntities(){
		GenericDao<T, PK> dao = getDao();
		if(dao == null)
			throw new java.lang.IllegalStateException("DAO not Injected");
        try {
			return dao.findAll();
		} catch (DaoException e) {
			addErrorMessage(e.getLocalizedMessage());
		}
        return null;
	}
	
	@Override
	public String delete() {
		beforeDelete();
		
		GenericDao<T, PK> dao = getDao();
		if(dao == null)
			throw new java.lang.IllegalStateException("DAO not Injected");
		if(logger.isLoggable(Level.INFO))
			logger.info("Removing " + getClass().getSimpleName() + " with Id: " + getEntityId());
    	try {
			dao.remove(entity);
			afterDelete();
			
			return deleteOk();
		} catch (DaoException e) {
			addErrorMessage(e.getLocalizedMessage());
	    	return deleteError();
		}
	}
	
	public boolean isRemoveOk(){
		return true;
	}	

	@Override
	public String persist() {
		beforePersist();
		
    	GenericDao<T, PK> dao = getDao();
		if(dao == null)
			throw new java.lang.IllegalStateException("DAO not Injected");

		try{
			if(validateEntity()){
				if(editing){
					beforeUpdate();
					dao.merge(entity);
					afterUpdate();
				}else{
					beforeCreate();
					if(validateBeforCreate()){
						dao.persist(entity);					
						afterCreate();
					}else{
						validateError();
						return currentView();
					}
				}
			}else{
				if(!StringUtils.isEmpty(validationErrorMessage)){
					addErrorMessage(this.validationErrorMessage);
				}else{
					final String errorMessage = ResourceBundleUtil.getValidationMessageString(DEFAULT_VALIDATION_MESSAGE);
					addErrorMessage(errorMessage);
				}
			}
		}catch (Exception e) {
			addErrorMessage(e.getLocalizedMessage());			
			return currentView();
		}	
		
		afterPersist();
		
		return persistOk();
    }
	
	@Override
    public void create(){
    	if(logger.isLoggable(Level.INFO))
    		logger.info("Create");
		this.entity = newEntity();
	}

	@Override
	public void refresh(){
    	if(logger.isLoggable(Level.INFO))
    		logger.info("Refreshing Entity: " + entity);
		try {
			setEntity(getDao().findById(getEntityId()));
		} catch (DaoException e) {
			addErrorMessage(e.getLocalizedMessage());
		}
	}
    
	
	/* Searcheable Bean */
	
	public SearchFilter getSearchFilter() {
		if(searchFilter == null)
			this.searchFilter = createSearchFilter();
		return this.searchFilter;
	}
	
	public void search() {
		if(this.searchFilter != null)
			this.searchFilter.setSearchActive(true);
		if(this.dataModel != null)
			this.dataModel.reset();
	}
	
	public boolean isSearchActive() {
		if(this.searchFilter == null)
			return false;
		return this.searchFilter.isSearchActive();
	}
	
	public ExtendedDataModel<T> getDataModel() {
		if(this.dataModel == null){
			if(logger.isLoggable(Level.FINE))
				logger.fine("Creating DataModel...");
			this.dataModel = new EntityDataModel();
		}
		return dataModel;
	}

	public List<T> getSearchResults() {
		if(getDao() == null)
			addErrorMessage("DAO Not Injected");
		try {
			return getDao().findAll();
		} catch (DaoException e) {
			addErrorMessage(e.getMessage());
		}
		return null;
	}
	
	@Override
	public abstract GenericConverter<T, PK> getConverter();
	
	@Override
	public String cancel() {
		return persistCancel();
	}
	
	@Override
	public String showCreate() {
		createBean();
		updateCancelView();		
		return facadePath(createAction());
	}

	@Override
	public String showRead() {
		if(!chechValidEntity())
			return facadePath(ERROR_ACTION);

		editing = false;
		updateCancelView();
		return facadePath(readAction());		
	}
	
	@Override
	public String showUpdate() {
		if(!chechValidEntity())
			return facadePath(ERROR_ACTION);

		editing = true;
		updateCancelView();
		return facadePath(updateAction());
	}
	
	@Override
	public String showDelete() {
		if(!chechValidEntity())
			return facadePath(ERROR_ACTION);

		editing = true;
		updateCancelView();
		return facadePath(deleteAction());
	}

	@Override
	public boolean isEditing(){
		return editing;
	}
	
	protected void setEditing(boolean editing){
		this.editing = editing;
	}

	
	/**
	 * Exporta el contenido a excel
	 */
	public void exportToExcel(){
		ResultExporter<T> exporter = new ExcelResultExporter<T>();
		
		exporter.exportResult(getExportColInfoList(), 
				getColumnValueMapper(), 
				getSearchResults(), 
				exportToExcelFile());
	}
	
	@Override
	public String showSearch() {
		this.searchFilter = null;
		beforeSearch();
		return facadePath(searchAction());
	}
	
	/**
	 * Construye la url en base al rol del usuario, prefijo de la entidad, y la acción a realizar.
	 * @param action acción del CRUD a realizar o acción que pase el usuario en el bean.
	 * @return url de la acción
	 */
	protected String facadePath(String action){
		return JSFMenuUtil.buildMenuPath(userRolePath(), entityPrefix(), action);
	}

	
	/**
	 * Template Method para obtener la Clave Primaria (Primary Key PK) de la
	 * entidad.
	 * <p>
	 * Las subclase deberán implementar el método abstracto que devuelva la clave e una 
	 * entidad dado. Éste método usa esta implementación para obtener la PK de la entidad
	 * actual ({@link #getEntity()}).
	 * </p>
	 * @return Clave primaria de la entidad actual del Bean.
	 */
	protected PK getEntityId(){
		return getEntityId(getEntity());
	}
	
	protected boolean chechValidEntity() {
		if((entity == null && id != null) || entity.isNew())
			loadEntityById();
		return (entity != null);
	}
	
	protected void loadEntityById() {
		try {
			setEntity(getDao().findById(id));
		} catch (DaoException e) {
			logger.severe(e.getMessage());
		}
	}
	
	
	protected T findEntity(T entity){
		if(entity == null)
			return null;
		return findEntityByName(getEntityName(entity));
	}
	
	/*
	 * Abstract Methods a implementar por las subclases.
	 */
	
	/**
	 * DAO utilizada para realizar el CRUD en el Bean.
	 * @return Subclase de {@link GenericDao} utilizado por la subclase.
	 */
	protected abstract GenericDao<T, PK> getDao();
	
	/**
	 * Factory Method para crear una nueva entidad del tipo <code>T</code>.
	 * @return Nueva entidad del Tipo <code>T</code>.
	 */
	protected abstract T newEntity();

	/**
	 * Las subclases deben devolver la clave primaria de la entidad pasada como parámetro.
	 * @param t Entidad sobre la cual de devolverá su clave primaria.
	 * @return Clave primaria del la entidad <code>t</code> del Tipo <code>T</code>.
	 */
	protected abstract PK getEntityId(T t);
	
	/**
     * Devuelve la cadena que se utilizar como prefijo de la entidad
     * para calcular las rutas de las paginas del CRUD.
	 * @return Rol del usuario que utiliza este Bean.
	 */
	protected abstract String entityPrefix();
	
	/**
     * Devuelve la cadena que se utilizará, en base al Rol del usuario, 
     * para calcular las rutas de las paginas del CRUD.
	 * @return Rol del usuario que utiliza este Bean.
	 */
	protected abstract String userRolePath();
	
	/**
	 * Factory Method para crear el filtro de búsqueda para este tipo de entidades.
	 * @return 
	 */
	protected abstract SearchFilter createSearchFilter();
	
	/**
	 * @param entity
	 * @return Nombre de la Entidad a persistir
	 */
	protected abstract String getEntityName(T entity);
	
	/**
	 * Busca una Entidad por su Nombre.
	 * <p>Se utiliza para determinar o prevenir el ingreso repetido
	 * de entidades en el sistema.</p>
	 * @param entityName
	 * @return
	 */
	protected abstract T findEntityByName(String entityName);
	
	
	/**
	 * Obtiene la List de Información para la Exportación de datos.
	 * @return
	 */
	protected abstract List<ExportColInfo> getExportColInfoList();
	
	/**
	 * Mapper de los valores de resultados a valores de columnas.
	 * <p>Cada Bean concreto deberá devolver un valore de la entidad
	 * por cada número de columna.
	 * </p>
	 * @return 
	 */
	protected abstract ColumnValueMapper<T> getColumnValueMapper();
	
	/*
	 * Hook Methods. 
     * Las subclases puede sobreescribir estos métodos para
     * modificar el comportamiento del Bean.		   
	 */

	// Hook methods for Update

	protected boolean validateEntity(){
		this.validationErrorMessage = null;
		return true;
	}
	
	// Hook methods for Persistence
	protected void beforePersist(){	
	}
	protected void afterPersist(){
	}

	// Hook methods for Create

	protected void beforeCreate(){	
	}
	protected void afterCreate(){
		FacesContext context = FacesContext.getCurrentInstance();
		Locale locale = context.getViewRoot().getLocale();
		String entityAddedMessage = ResourceBundleUtil.getResourceBundleString(getEntityAddedMessage(), locale);
		addInfoMessage(entityAddedMessage);
	}

	// Hook methods for Update

	protected void beforeUpdate(){	
	}
	protected void afterUpdate(){
		FacesContext context = FacesContext.getCurrentInstance();
		Locale locale = context.getViewRoot().getLocale();
		String entityAddedMessage = ResourceBundleUtil.getResourceBundleString(getEntityUpdatedMessage(), locale);
		addInfoMessage(entityAddedMessage);
	}

	// Hook methods for Read
	protected void beforeRead() {
	}

	// Hook methods for deletion
	
	protected void beforeDelete(){	
	}
	protected void afterDelete(){
		FacesContext context = FacesContext.getCurrentInstance();
		Locale locale = context.getViewRoot().getLocale();
		String entityRemovedMessage = ResourceBundleUtil.getResourceBundleString(getEntityRemovedMessage(), locale);
		addInfoMessage(entityRemovedMessage);
	}
	
	// Hook methods for Search
	protected void beforeSearch() {
		if(this.dataModel != null)
			reset();
	}
	
	protected String persistOk() {
		if(persistReturnView != null && !persistReturnView.isEmpty())
			return persistReturnView + JSFMenuUtil.JSF_REDIRECT;
		return showSearch();
	}
	 
	protected String persistCancel() {
		if(cancelView != null && !cancelView.isEmpty()) 
			return cancelView + JSFMenuUtil.JSF_REDIRECT;
		return showSearch();		
	}
	
	protected String persistError() {
		return facadePath(ERROR_ACTION);
	}

	protected String exportToExcelFile() {
		return entityPrefix() + "." + FacesUtil.XLS_FILE_TYPE;
	}

	protected String exportToCSVFile() {
		return entityPrefix() + "." + FacesUtil.CSV_FILE_TYPE;
	}

	protected String deleteOk() {
		if(persistReturnView != null && !persistReturnView.isEmpty())
			return persistReturnView;
		return showSearch();
	}
	
	protected String deleteError() {
		if(persistReturnView != null && !persistReturnView.isEmpty())
			return persistReturnView;
		return showSearch();		
	}
	
	protected String searchAction() {
		return INDEX_ACTION;
	}

	protected String createAction() {
		return CREATE_ACTION;
	}
	
	protected String readAction() {
		return SHOW_ACTION;
	}

	protected String updateAction() {
		return EDIT_ACTION;
	}
	
	protected String deleteAction() {
		return REMOVE_ACTION;
	}

	
	protected void updateCancelView(){
		FacesContext ctx = FacesContext.getCurrentInstance();
		cancelView = ctx.getViewRoot().getViewId();
	}

	public String getCancelView() {
		return cancelView;
	}

	public void setCancelView(String cancelView) {
		this.cancelView = cancelView;
	}

	public String getPersistReturnView() {
		return persistReturnView;
	}

	public void setPersistReturnView(String persistReturnView) {
		this.persistReturnView = persistReturnView;
	}
	
	protected String currentView() {
		return FacesContext.getCurrentInstance().getViewRoot().getViewId();
	}

	public static void addErrorMessage(String localizedMessage) {
		addMessage(localizedMessage, FacesMessage.SEVERITY_ERROR);
	}	

	public static void addInfoMessage(String localizedMessage) {
		addMessage(localizedMessage, FacesMessage.SEVERITY_INFO);
	}
	
	public static void addMessage(String localizedMessage, Severity severity) {
		addMessage(null, localizedMessage, severity);
	}
	
	public static void addMessage(String clientId, String localizedMessage, Severity severity) {
		FacesContext context = FacesContext.getCurrentInstance();
		UIComponent componet = null;
		if(clientId != null)
			componet = context.getViewRoot().findComponent(clientId);
		
		FacesMessage message = new FacesMessage();
		message.setSeverity(severity);
		message.setSummary(localizedMessage);
		message.setDetail(localizedMessage);
		if(componet == null)
			context.addMessage(null, message);
		else
			context.addMessage(componet.getClientId(), message);
	}

	
	// hook method for remove
	protected boolean removeEntityDependencies() {
		return true;
	}

	// hook method for remove
	protected boolean hasDependencies() {
		return false;
	}
	
	public boolean isEntityWithDependencies(){
		return hasDependencies();
	}
	
	protected String getEntityRepeteadMessage() {
		return getEntityMessagePrefix() + REPEATED_ENTITY_ERROR_MESSAGE_SUFIX;
	}

	protected String getEntityAddedMessage() {
		return getEntityMessagePrefix() + ADDED_MESSAGE_SUFIX;
	}

	protected String getEntityUpdatedMessage() {
		return getEntityMessagePrefix() + UPDATED_MESSAGE_SUFIX;
	}

	protected String getEntityRemovedMessage() {
		return getEntityMessagePrefix() + REMOVED_MESSAGE_SUFIX;
	}
	
	protected void validateError() {
		Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
		String entityRepeteadMessage = ResourceBundleUtil.getResourceBundleString(getEntityRepeteadMessage(), locale);
		addErrorMessage(entityRepeteadMessage);
	}

	protected boolean validateBeforCreate() {
		if(!validateEntityNoRepeated()){
			return false;
		}
		return true;
	}
	
	protected boolean validateEntityNoRepeated() {
		if(isValidateRepeatedEntityBeforeCreate()){
			T repeatedEntity = findEntity(entity);
			return (repeatedEntity == null);
		}
		return true;
	}

	protected boolean isValidateRepeatedEntityBeforeCreate() {
		return true;
	}
	
	/**
	 * Sobreescribir por si hay conflictos en Resource Bundle
	 * @return
	 */
	protected String getEntityMessagePrefix(){
		return entityPrefix();
	}

	private class EntityDataModel extends ExtendedDataModel<T>  {
		private Long rowKey = null;
		private int rowCount = -1;
		private List<T> cache = null;
		private int currentRow = -1;
		private int rows = -1;
		
		public EntityDataModel() {
			super();
			reset();
		}
		
		@Override
		public Object getRowKey() {
			return rowKey;
		}

		@Override
		public void setRowKey(Object rowKey) {
			this.rowKey = (Long)rowKey;
		}

		@Override
		public void walk(FacesContext fc, DataVisitor dv, Range range, Object o) {
	        SequenceRange sr = (SequenceRange) range;
	        int currentRow = sr.getFirstRow();
	        int rows = sr.getRows();
	        
			if(logger.isLoggable(Level.FINE))
				logger.fine(String.format("Walking from %d to %d", currentRow, (currentRow+rows)));
			
			try {
				if(this.currentRow != currentRow || this.rows != rows || this.cache == null){
					if(searchFilter != null){
						this.cache = getDao().findEntriesBySearchFilter(searchFilter, currentRow, rows);
					}else{
						this.cache = getDao().findEntries(currentRow, rows);
					}
					if(cache == null){
						logger.warning("Cache is null");
						this.rowCount = -1;
					}else{
						if(logger.isLoggable(Level.FINE))
							logger.fine(String.format("Cache size is %d.", cache.size()));
						if(this.rowCount == -1)
							this.rowCount = cache.size();
					}
				}
				
		        for (T t : cache) {
		        	dv.process(fc, getEntityId(t), o);
		        }
			} catch (DaoException e) {
				logger.severe(e.getMessage());
				addErrorMessage(e.getMessage());
			}
			this.currentRow = currentRow;
			this.rows = rows;
		}

		@Override
		public int getRowCount() {
			if(rowCount == -1){
				try {
					if(searchFilter != null){
						rowCount = (int)getDao().countBySearchFilter(searchFilter);
					}else {
						rowCount = (int)getDao().count();
					}
				} catch (DaoException e) {
					logger.severe(e.getMessage());
					addErrorMessage(e.getMessage());
				}
				if(logger.isLoggable(Level.FINE))
					logger.fine("Row count: " + rowCount);
			}
			return rowCount;
		}

		@Override
		public T getRowData() {
			for(T t : cache){
				if(getEntityId(t).equals(rowKey))
					return t;
			}
			return null;
		}

		@Override
		public int getRowIndex() {
			return -1;
		}

		@Override
		public Object getWrappedData() {
			throw new UnsupportedOperationException();
		}

		@Override
		public boolean isRowAvailable() {
			return rowKey != null;
		}

		@Override
		public void setRowIndex(int arg0) {
			throw new UnsupportedOperationException();
		}

		@Override
		public void setWrappedData(Object arg0) {
			throw new UnsupportedOperationException();
		}

		public void reset() {
			this.rowKey = null;
			this.rowCount = -1;
			this.cache = null;
			this.currentRow = -1;
			this.rows = -1;
		}
	}
	
	public boolean isSelected() {
		return selected;
	}

	public void setSelected(boolean selected) {
		this.selected = selected;
	}
	
	public void sort() {
        String property = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get(SORT_PROPERTY_PARAMETER);
        if (property != null) {
            SortOrder currentPropertySortOrder = sortsOrders.get(property);
            if (multipleSorting) {
                if (!sortPriorities.contains(property)) {
                    sortPriorities.add(property);
                }
            } else {
                sortsOrders.clear();
            }
            if (currentPropertySortOrder == null || currentPropertySortOrder.equals(SortOrder.descending)) {
                sortsOrders.put(property, SortOrder.ascending);
            } else {
                sortsOrders.put(property, SortOrder.descending);
            }
        }
    }
 
    public void modeChanged(ValueChangeEvent event) {
        reset();
    }
 
    public void reset() {
        sortPriorities.clear();
        sortsOrders.clear();
    }
 
    public boolean isMultipleSorting() {
        return multipleSorting;
    }
 
    public void setMultipleSorting(boolean multipleSorting) {
        this.multipleSorting = multipleSorting;
    }
 
    public List<String> getSortPriorities() {
        return sortPriorities;
    }
 
    public Map<String, SortOrder> getSortsOrders() {
        return sortsOrders;
    }
}